﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2014-02-18 15:10
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BassDrop.Core.Common.Exceptions.Cloud;

namespace BassDrop.Core.Common.Cloud.Requests
{
	/// <summary>
	/// A cloud request that executes a list of other requests in order
	/// </summary>
	public class CloudRequestCompound : CloudRequestBase
	{
		private List<CloudRequestBase> m_requests = new List<CloudRequestBase>();
		private List<CloudRequestBase> m_ensuredRequests = new List<CloudRequestBase>();

		/// <summary>
		/// Returns the child requests in this compound request as an enumerable
		/// </summary>
		public IEnumerable<CloudRequestBase> Requests
		{
			get
			{
				return m_requests.AsEnumerable();
			}
		}

		/// <summary>
		/// Returns the ith child request of this compound request
		/// </summary>
		/// <param name="i"></param>
		/// <returns></returns>
		public CloudRequestBase this[int i]
		{
			get
			{
				return m_requests[i];
			}
		}

		/// <summary>
		/// Constructs a new compound request
		/// </summary>
		/// <param name="mergeable">Is this request allowed to be merged with others</param>
		public CloudRequestCompound( bool mergeable = true )
			: base( mergeable )
		{
		}

		/// <summary>
		/// Appends a new request at the end of the list
		/// </summary>
		/// <param name="request"></param>
		/// <param name="ensure">
		///		ensure that the request is completed no matter what, even if requests before it threw exceptions.
		///		If normal execution fails, these requests get executed IN NO PARTICULAR ORDER.
		/// </param>
		public void Add( CloudRequestBase request, bool ensure = false )
		{
			m_requests.Add( request );

			if ( ensure )
			{
				m_ensuredRequests.Add( request );
			}
		}

		/// <summary>
		/// Appends a list of new request at the end of the list
		/// </summary>
		/// <param name="requests"></param>
		public void AddRange( IEnumerable<CloudRequestBase> requests )
		{
			m_requests.AddRange( requests );
		}

		/// <summary>
		/// Returns if the passed request can be merged into this one
		/// </summary>
		/// <param name="other"></param>
		/// <returns></returns>
		protected override bool CanMerge( CloudRequestBase other )
		{
			if ( other is CloudRequestCompound )
			{
				return CanMergeCompound( (CloudRequestCompound) other );
			}
			else
			{
				foreach ( var item in m_requests )
				{
					if ( CanMerge( item, other ) )
					{
						return true;
					}
				}
			}

			return false;
		}

		/// <summary>
		/// Returns if the passed compound request can be merged into this one or this one can be merged into the other
		/// </summary>
		/// <param name="other"></param>
		/// <returns></returns>
		private bool CanMergeCompound( CloudRequestCompound other )
		{
			return MergeLocation( other ) != null;
		}

		/// <summary>
		/// Returns the place where the passed compound request can be merged into this one.
		/// Null is returned if it cannot be merged.
		/// A positive number, the start index of the merge inside this object is returned if it can be merged.
		/// A negative number, the start index * -1 of the merge inside the other object is returned, if this can be merged into the other object.
		/// </summary>
		/// <param name="other"></param>
		/// <param name="skipOtherDirection">skip checkin if this can be merged into the other to prevent an infinite loop</param>
		/// <returns></returns>
		private int? MergeLocation( CloudRequestCompound other, bool skipOtherDirection = false )
		{
			for ( int i = 0; i < m_requests.Count; i++ )
			{
				for ( int j = 0; j < other.m_requests.Count; j++ )
				{
					if ( i + j >= m_requests.Count )
					{
						return i;
					}
					else if ( !CanMerge( m_requests[i + j], other.m_requests[j] ) )
					{
						break;
					}
					else if ( j == other.m_requests.Count - 1 ) // this was the last in the other's series
					{
						return i;
					}
				}
			}

			if ( !skipOtherDirection )
			{
				int? otherIndex = other.MergeLocation( this, true );
				return otherIndex != null ? otherIndex * -1 : null;
			}
			else
			{
				return null;
			}
		}

		/// <summary>
		/// Merges the passed request into this one.
		/// May throw various exceptions if it cannot be merged, call CanMerge first to check if it can be done.
		/// </summary>
		/// <param name="other"></param>
		protected override void DoMerge( CloudRequestBase other )
		{
			var compoundOther = other as CloudRequestCompound;

			if ( compoundOther != null )
			{
				int? mergeLocation = MergeLocation( compoundOther );

				if ( mergeLocation == null )
				{
					throw new InvalidOperationException( "The passed request cannot be merged with this one" );
				}
				else
				{
					List<CloudRequestBase> requests = new List<CloudRequestBase>();

					if ( mergeLocation < 0 )
					{
						mergeLocation *= -1;

						requests.AddRange( compoundOther.m_requests );

						int j = 0;

						for ( int i = (int) mergeLocation; i < compoundOther.m_requests.Count && j < m_requests.Count; i++, j++ )
						{
							Merge( requests[i], m_requests[j] );
						}

						for ( ; j < m_requests.Count; j++ )
						{
							requests.Add( m_requests[j] );
						}
					}
					else
					{
						requests.AddRange( m_requests );

						int j = 0;

						for ( int i = (int) mergeLocation; i < m_requests.Count && j < compoundOther.m_requests.Count; i++, j++ )
						{
							Merge( requests[i], compoundOther.m_requests[j] );
						}

						for ( ; j < compoundOther.m_requests.Count; j++ )
						{
							requests.Add( compoundOther.m_requests[j] );
						}
					}

					m_requests = requests;

					foreach ( var otherRequest in compoundOther.m_ensuredRequests )
					{
						bool merged = false;

						foreach ( var request in m_ensuredRequests )
						{
							if ( request.TryMerge( otherRequest ) )
							{
								merged = true;
								break;
							}
						}

						if ( !merged )
						{
							m_ensuredRequests.Add( otherRequest );
						}
					}
				}
			}
		}

		/// <summary>
		/// Determines if retrying the execution of this request may succeed given the passed
		/// exception has been thrown on the last try.
		/// </summary>
		/// <param name="e"></param>
		/// <returns></returns>
		protected override bool CanRepair( Exception e )
		{
			return false; // If it could be repaired, the Execute method would have done it and wouldn't have thrown an exception
		}

		/// <summary>
		/// Executes this request on the passed connection
		/// </summary>
		/// <param name="connection"></param>
		protected override async Task DoExecute( CloudConnectionBase connection )
		{
			Exception error = null;

			foreach ( var item in m_requests )
			{
				do // Auto retry if it may succeed next time
				{
					try
					{
						await Execute( item, connection );
						m_ensuredRequests.Remove( item );
						break;
					}
					catch ( Exception e )
					{
						if ( !CanRepair( item, e ) )
						{
							error = e;
							// break out of foreach
							goto ensure;
						}
						else
						{
							// TODO: sleep?
						}
					}
				} while ( true );
			}

		// >>> GOTO from foreach -> do -> catch -> if
		ensure:
			// Execute leftover ensured requests
			foreach ( var item in m_ensuredRequests )
			{
				do // Auto retry if it may succeed next time
				{
					try
					{
						await Execute( item, connection );
						break;
					}
					catch ( Exception e )
					{
						if ( !CanRepair( item, e ) )
						{
							// execution errors are signalled in an event if the scheduler thinks it's necessary
							break;
						}
						else
						{
							// TODO: sleep?
						}
					}
				} while ( true );
			}

			if ( error != null )
			{
				throw error;
			}
		}
	}
}
